('Negative', 'Mixed', 'Mostly Positive', 'Positive', 'Very Positive'). La métrica de evaluación utilizada para medir la clasificación es f1_macro.r_2.El objetivo de este proyecto consiste en resolver dos problemas distintos asociados al mundo del cine, los cuales servirán para determinar el éxito o fracaso de una película. El primer problema consiste en implementar un modelo de clasificación capaz de asignar potenciales evaluaciones a las películas en distintos niveles de aceptación. Por otro lado, el segundo problema busca poder estimar el posible ingreso que podría generar cada película por medio de un modelo de regresión.
Los datos que se proveen, para poder llevar a cabo lo recién mencionado, corresponden a dos datasets con un total de 9641 ejemplos cada uno, los cuales presentan atributos numéricos y categóricos correspondientes a caracterísitcas particulares de cada película. Al juntar ambos datasets en función de los id se llegan a tener un total de 21 atributos, de los cuales se eliminaron 7 dado que algunos se repetían y otros presentaban información redundante e innecesario. Además, se realizó un procesamiento de la mayoría de las variables. De los 14 atributos restantes, 2 de ellos representan las variables objetivos de los modelos, las cuales son label, que es de tipo categórica y se utiliza para el problema de clasificación, y target que representa el ingreso de la película, es de tipo numérica y se usa para el modelo de regresión.
Con respecto a la evaluación del rendimiento de los modelos, se tiene que el de clasificación se evalua en base a la métrica f1_macro, ya que esta permite medir de forma simultánea las métricas de precisión y recall aplicadas sobre las predicciones realizadas por el modelo para un problema multiclase, lo cual sirve para determinar de mejor manera el desempeño cuando se tienen datos desbalanceados (como es el caso del problema de clasificación). Por otro lado, la tarea de regresión se evalúa por medio de la métrica R2 score, que permite determinar la calidad del modelo para replicar los resultados, y la proporción de variación de los resultados que puede explicarse por el modelo.
Nuestra propuesta para resolver el problema consistió en realizar un EDA inicial, con el fin de determinar características importantes de los datos que posteriormente se pudieran usar para el entrenamiento de los modelos. Una vez obtenida la información de los datos, se prosiguió a generar una función de features propia que nos permitiera extraer las caracterísitcas más útiles para realizar la clasificación y regresión. Con respecto a la selección de modelos, para la primera tarea se utilizaron en primer lugar algunos algoritmos básicos como clasificadores Dummy, DecisionTree, Random Forest y SVC, los cuales fueron tuneados mediante un GridSearch. Dado que se obtuvieron rendimiento bajos, se recurrió al uso de clasificadores XGBoost y LightGBM, los cuales también fueron tuneados vía GridSearch. Por otro lado, para la tarea de regresión, se siguió un procedimiento similar, pues se testeo primero la resolución del problema con los regresores básicos Dummy y DecisionTree. Sin embargo, puesto que el rendimiento de modelos XGBoost y LightGBM sobrepasaba a los clásicos en la tarea de clasificación, se decidió realizar un GridSearch únicamente para regresores de este tipo.
Para ambos problemas se obtuvo un mejor rendimiento con los modelos de XGBoost, los cuales fueron capaces de cumplir los objetivos del proyecto. Si bien los resultados obtenidos con un conjunto de validación no alcanzaron un rendimiento óptimo, siendo 0.33 el f1 score en clasificación y 0.566 R2 score en regresión, los resultados con el conjunto de test para la competencia fueron más que satisfactorios, dado que se obtuvieron scores de 0.96 y 0.89 para f1 y R2 respectivamente.
## Imports
import numpy as np
import pandas as pd
from sklearn.pipeline import Pipeline
from sklearn.model_selection import train_test_split
# Pre-procesamiento
from sklearn.feature_selection import SelectPercentile, f_classif, f_regression
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import MinMaxScaler,StandardScaler
from sklearn.preprocessing import OrdinalEncoder, OneHotEncoder
from sklearn.preprocessing import FunctionTransformer
from sklearn.feature_extraction.text import CountVectorizer
# Clasifación y regresión
from sklearn.dummy import DummyClassifier, DummyRegressor
from sklearn.svm import SVC, SVR
from sklearn.naive_bayes import MultinomialNB
from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor
from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression, ElasticNet, Ridge
# Metricas de evaluación
from sklearn.metrics import classification_report
from sklearn.metrics import accuracy_score
from sklearn.metrics import f1_score
from sklearn.metrics import cohen_kappa_score
from sklearn.metrics import r2_score
# Librería para plotear
# !pip install --upgrade plotly
import plotly.express as px
from plotly.subplots import make_subplots
import plotly.graph_objects as go
# Proyecciones en baja dimensionalidad: UMAP
# !pip install umap-learn
import umap
# Librería para NLP
# !pip install nltk
import nltk
from nltk.corpus import stopwords
from nltk import word_tokenize
from nltk.stem import PorterStemmer
nltk.download('stopwords')
nltk.download('punkt')
# Librerias modelos boosting
# !pip install xgboost
# !pip install lightgbm
from xgboost import XGBClassifier, XGBRegressor
from lightgbm import LGBMClassifier, LGBMRegressor
import seaborn as sns
import matplotlib.pyplot as plt
import missingno as msno
import time
# import warnings
# warnings.filterwarnings("ignore")
[nltk_data] Downloading package stopwords to [nltk_data] C:\Users\tacom\AppData\Roaming\nltk_data... [nltk_data] Package stopwords is already up-to-date! [nltk_data] Downloading package punkt to [nltk_data] C:\Users\tacom\AppData\Roaming\nltk_data... [nltk_data] Package punkt is already up-to-date!
## Código Preparación de Datos.
## Load of data
num_data = pd.read_parquet('train_numerical_features.parquet', engine='pyarrow')
text_data = pd.read_parquet('train_text_features.parquet', engine='pyarrow')
full_data = num_data.merge(text_data,how='inner',on='id',suffixes=('','_y'))
print('Load of data')
print(list(num_data.columns),'\n',num_data.shape)
print(list(text_data.columns),'\n',text_data.shape)
print(list(full_data.columns),'\n',full_data.shape,'\n')
## Eliminando columnas
columns2drop = ['title_y','tagline_y','credits_y','poster_path', 'backdrop_path', 'recommendations']
full_data = full_data.drop(columns=columns2drop)
print('Eliminando columnas')
print(list(full_data.columns),'\n',full_data.shape,'\n')
## Filtrar por revenue igual a cero
full_data = full_data.drop(full_data[full_data['revenue']==0].index,axis=0)
print('Filtrar por revenue igual a cero')
print(list(full_data.columns),'\n',full_data.shape,'\n')
## Filtrar por release_date y runtime nulos
full_data = full_data.drop(full_data[(full_data['release_date'].isnull())|(full_data['runtime'].isnull())].index,axis=0)
print('Filtrar por release_date y runtime nulos')
print(list(full_data.columns),'\n',full_data.shape,'\n')
## Cambiando columna realease_date a datetime
full_data['release_date'] = pd.to_datetime(full_data['release_date'].astype(str),format="%Y/%m/%d")
## Eliminando status distintos a released
full_data = full_data.drop(full_data[full_data['status']!='Released'].index,axis=0)
print('Eliminando status distintos a released')
print(list(full_data.columns),'\n',full_data.shape,'\n')
## Rellenar valores nulos categóricos y de texto
full_data.fillna('',axis=0,inplace=True)
## Generando labels
bins = [0, 5, 6, 7, 8, 10]
labels = ['Negative','Mixed','Mostly Positive','Positive','Very Positive']
full_data['label'] = pd.cut(full_data['vote_average'], bins=bins, labels=labels)
## Eliminando columnas vote_average y id
full_data = full_data.drop(columns=['vote_average','id'])
print('Eliminando columnas vote_average y id')
print(list(full_data.columns),'\n',full_data.shape,'\n')
## Renombrando revenue como target
full_data.rename(columns={'revenue':'target'},inplace=True)
print('Renombrando revenue como target')
print(list(full_data.columns),'\n',full_data.shape,'\n')
Load of data ['id', 'title', 'budget', 'revenue', 'runtime', 'status', 'tagline', 'credits', 'poster_path', 'backdrop_path', 'recommendations'] (9641, 11) ['id', 'title', 'genres', 'original_language', 'overview', 'production_companies', 'release_date', 'tagline', 'credits', 'keywords', 'vote_average'] (9641, 11) ['id', 'title', 'budget', 'revenue', 'runtime', 'status', 'tagline', 'credits', 'poster_path', 'backdrop_path', 'recommendations', 'title_y', 'genres', 'original_language', 'overview', 'production_companies', 'release_date', 'tagline_y', 'credits_y', 'keywords', 'vote_average'] (9641, 21) Eliminando columnas ['id', 'title', 'budget', 'revenue', 'runtime', 'status', 'tagline', 'credits', 'genres', 'original_language', 'overview', 'production_companies', 'release_date', 'keywords', 'vote_average'] (9641, 15) Filtrar por revenue igual a cero ['id', 'title', 'budget', 'revenue', 'runtime', 'status', 'tagline', 'credits', 'genres', 'original_language', 'overview', 'production_companies', 'release_date', 'keywords', 'vote_average'] (6451, 15) Filtrar por release_date y runtime nulos ['id', 'title', 'budget', 'revenue', 'runtime', 'status', 'tagline', 'credits', 'genres', 'original_language', 'overview', 'production_companies', 'release_date', 'keywords', 'vote_average'] (6451, 15) Eliminando status distintos a released ['id', 'title', 'budget', 'revenue', 'runtime', 'status', 'tagline', 'credits', 'genres', 'original_language', 'overview', 'production_companies', 'release_date', 'keywords', 'vote_average'] (6451, 15) Eliminando columnas vote_average y id ['title', 'budget', 'revenue', 'runtime', 'status', 'tagline', 'credits', 'genres', 'original_language', 'overview', 'production_companies', 'release_date', 'keywords', 'label'] (6451, 14) Renombrando revenue como target ['title', 'budget', 'target', 'runtime', 'status', 'tagline', 'credits', 'genres', 'original_language', 'overview', 'production_companies', 'release_date', 'keywords', 'label'] (6451, 14)
## Código EDA
## Viendo types de columnas
full_data.dtypes
title object budget float64 target float64 runtime float64 status object tagline object credits object genres object original_language object overview object production_companies object release_date datetime64[ns] keywords object label category dtype: object
full_data[['budget','target','runtime']].hist(bins=89,figsize=(15,7))
array([[<AxesSubplot:title={'center':'budget'}>,
<AxesSubplot:title={'center':'target'}>],
[<AxesSubplot:title={'center':'runtime'}>, <AxesSubplot:>]],
dtype=object)
fig = px.histogram(full_data, x="label",width=800, height=400,title='Histograma de Label')
fig.show()
fig = px.histogram(full_data, x="original_language",width=800, height=400,title='Histograma de Lenguaje original')
fig.show()
pd.plotting.scatter_matrix(full_data,alpha=0.9,figsize=(15,10))
array([[<AxesSubplot:xlabel='budget', ylabel='budget'>,
<AxesSubplot:xlabel='target', ylabel='budget'>,
<AxesSubplot:xlabel='runtime', ylabel='budget'>],
[<AxesSubplot:xlabel='budget', ylabel='target'>,
<AxesSubplot:xlabel='target', ylabel='target'>,
<AxesSubplot:xlabel='runtime', ylabel='target'>],
[<AxesSubplot:xlabel='budget', ylabel='runtime'>,
<AxesSubplot:xlabel='target', ylabel='runtime'>,
<AxesSubplot:xlabel='runtime', ylabel='runtime'>]], dtype=object)
worthy_columns = ['budget','target','runtime','genres','original_language','label']
corr_matrix = full_data[worthy_columns].apply(lambda x : pd.factorize(x)[0]).corr(method='pearson', min_periods=1)
sns.set(rc = {'figure.figsize':(15,8)})
ax = sns.heatmap(
corr_matrix,
vmin=-1, vmax=1, center=0,
cmap=sns.diverging_palette(20, 220, n=200),
square=True
)
ax.set_xticklabels(
ax.get_xticklabels(),
rotation=45,
horizontalalignment='right'
)
[Text(0.5, 0, 'budget'), Text(1.5, 0, 'target'), Text(2.5, 0, 'runtime'), Text(3.5, 0, 'genres'), Text(4.5, 0, 'original_language'), Text(5.5, 0, 'label')]
tabla = pd.crosstab(
index=full_data["label"],
columns=full_data["original_language"],
)
tabla
| original_language | ab | af | ar | bn | cn | da | de | el | en | es | ... | no | pl | pt | ro | ru | sv | te | th | tr | zh |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| label | |||||||||||||||||||||
| Negative | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 157 | 0 | ... | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
| Mixed | 1 | 0 | 0 | 0 | 1 | 1 | 2 | 0 | 1259 | 7 | ... | 1 | 0 | 1 | 0 | 2 | 0 | 0 | 1 | 1 | 1 |
| Mostly Positive | 0 | 1 | 0 | 0 | 16 | 6 | 11 | 0 | 2682 | 28 | ... | 5 | 1 | 2 | 1 | 12 | 7 | 0 | 1 | 0 | 14 |
| Positive | 0 | 0 | 1 | 0 | 12 | 7 | 24 | 2 | 1373 | 47 | ... | 3 | 2 | 6 | 1 | 9 | 10 | 2 | 4 | 4 | 18 |
| Very Positive | 0 | 0 | 1 | 1 | 1 | 0 | 1 | 0 | 116 | 3 | ... | 0 | 0 | 5 | 0 | 2 | 0 | 0 | 0 | 1 | 1 |
5 rows × 34 columns
fig, ax = plt.subplots(figsize=[15, 10])
msno.matrix(full_data, ax=ax, sparkline=False)
<AxesSubplot:>
reducer = umap.UMAP(metric='chebyshev',random_state=4)
worthy_columns = ['budget','target','runtime']#,'genres','original_language']
umap_train = full_data[worthy_columns].values#.apply(lambda x : pd.factorize(x)[0]).values
scaled_umap_train = MinMaxScaler().fit_transform(umap_train)
embedding = reducer.fit_transform(scaled_umap_train)
embedding.shape
(6451, 2)
map_dict = {'Negative':0,'Mixed':1,'Mostly Positive':2,'Positive':3,'Very Positive':4}
plt.scatter(
embedding[:, 0],
embedding[:, 1],
c=[sns.color_palette()[x] for x in full_data.label.map(map_dict)])
plt.gca().set_aspect('equal', 'datalim')
plt.title('UMAP projection of the Movie Dataset', fontsize=24)
Text(0.5, 1.0, 'UMAP projection of the Movie Dataset')
fig = px.scatter(full_data,x='budget', y='target',title='Scatterplot Budget vs. Target')
fig.show()
sub_data = full_data.loc[full_data.production_companies.str.contains('Marvel', regex=False),:]
fig = px.scatter(sub_data,x='budget', y='target',title='Scatterplot Budget vs. Target para películas de Marvel',hover_name='title')
fig.show()
sub_data = full_data.loc[full_data.production_companies.str.contains('Warner', regex=False),:]
fig = px.scatter(sub_data,x='budget', y='target',title='Scatterplot Budget vs. Target para películas de Warner',hover_name='title')
fig.show()
sub_data = full_data['production_companies'].apply(lambda x: x.split('-'))
list_productoras = pd.Series(sum(list(sub_data.values), []))
productoras = pd.DataFrame({'productoras':list_productoras,'counts':np.zeros(len(list_productoras))})
productoras_data = productoras.groupby('productoras',as_index=False).count()
productoras_data = productoras_data.sort_values(by='counts',ascending=False)[:50]
top_productoras = productoras_data.values
fig = px.bar(productoras_data, x="productoras",y='counts',width=900, height=800,title='Top 50 productoras')
fig.show()
sub_data = full_data['credits'].apply(lambda x: x.split('-'))
list_actors = pd.Series(sum(list(sub_data.values), []))
actors = pd.DataFrame({'actores':list_actors,'counts':np.zeros(len(list_actors))})
actors_data = actors.groupby('actores',as_index=False).count()
# Se descartan los nombres que probablemente correspondían a un nombre compuesto
actors_data['nchars'] = actors_data.actores.apply(lambda x: len(x.split(' ')))
actors_data = actors_data.loc[actors_data.nchars>1,:]
actors_data = actors_data.sort_values(by='counts',ascending=False)[:50]
top_artistas = actors_data.values
fig = px.bar(actors_data, x="actores",y='counts',width=900, height=800,title='Top 50 artistas')
fig.show()
sub_data = full_data['keywords'].apply(lambda x: x.split('-'))
list_keywords = pd.Series(sum(list(sub_data.values), []))
keywords = pd.DataFrame({'keywords':list_keywords,'counts':np.zeros(len(list_keywords))})
keywords_data = keywords.groupby('keywords',as_index=False).count()
keywords_data = keywords_data.sort_values(by='counts',ascending=False)[:50]
top_keywords = keywords_data.values
fig = px.bar(keywords_data, x="keywords",y='counts',width=900, height=800,title='Top 50 keywords')
fig.show()
sub_data = full_data['genres'].apply(lambda x: x.split('-'))
list_genres = pd.Series(sum(list(sub_data.values), []))
genres = pd.DataFrame({'genres':list_genres,'counts':np.zeros(len(list_genres))})
genres_data = genres.groupby('genres',as_index=False).count()
genres_data = genres_data.sort_values(by='counts',ascending=False)[:50]
top_genres = genres_data.values
fig = px.bar(genres_data, x="genres",y='counts',width=900, height=800,title='Top 50 genres')
fig.show()
Análisis del EDA
En primera instancia, es necesario verificar y corregir cierta información errónea o no útil dentro de la base de datos, es por esto que se procede a eliminar las columnas de 'title_y','tagline_y','credits_y','poster_path', 'backdrop_path', 'recommendations', además, se filtran y mantienen las columnas con fechas y recaudación no nulas, finalmente, se genera una categorización de las notas de usuarios por cinco categorías más generales y se designan como labels del dataset de clasificación.
Respecto a las nuevas labels categóricas, se puede apreciar un gran desbalance de clases, donde la clase 'Mostly Positive' contiene 2983 entradas, mientras que las clases 'Very Positive' y 'Negative' tan sólo contienen 185 y 175 respectivamente. Esto afectará en gran medida el desempeño de los clasificadores, principalmente por el sobre-ajuste a las clases más predominantes. Para monitorear esto será sumamente importante observar el comportamiento de la métrica Recall o F1 para las clases menos predominantes. Para la presencia de idiomas, es notable que el idioma inglés predomina por sobre el resto.
En la dispersión de datos según dos variables, es decir, en un scatter plot, se puede identificar grandes acumulaciones de datos para valores bajos de budget con target, y ciertos valores alejados de la tendencia. Algo similar se puede osbservar al relacionar las variables de target y budget con la duración de la película, donde la mayor parte de los datos se observa alrededor del intervalo 100-150 minutos. Para la correlación de datos, en la matriz de correlación se puede observar que las variables con mayor correlación positiva respecto a target son genres, budget y original_language, donde esta última ya se pudo ver con bastante desbalance.
Al generar una reducción de dimensionalidad y scatter plot con UMAP, con las variables target, budget y runtime, se puede observar que no existen clusters muy identificables, por lo que será necesario un procesamiento de las variables no numéricas para complementar la caracterización de los conjuntos o labels.
Finalmente, respecto a la presencia de entidades importantes, tanto para las productoras, artistas, keywords y géneros se hizo una limpieza de datos no representativos o mal registrados, además de una separación por carácteres (por ejemplo: '-') para diferenciar entre entidades individuales y no agrupaciones. Es con esto que se puede observar la presencia de grandes productoras con presencia en la gran mayoría de películas registradas pero también una gran variedad de otras que son pseudónimos de la misma empresa pero que recaen en otra categoría (por ejemplo: Marvel y Walt Disney Pictures). Para los artistas, existían varios cuyos nombres no tenían identificación por apellido o al menos no fue posible rescatar de forma automática, por lo que no son considerados dentro del top 50, de forma similar ocurrió para las keywords y destacar que los top géneros de película son en categorías singulares y no compuestas, con la mayor presencia de películas de 'Drama'.
## Código Holdout
from sklearn.preprocessing import LabelEncoder
# Quitando la columna label de la base de datos a utilizar
X = full_data.drop(columns=['label','title'])
y = full_data['label']
X_train, X_test, ylabel_train, ylabel_test = train_test_split(X,y, stratify=y, test_size=0.2,shuffle=True,random_state=7)
# Encoder numérico para las labels
label_encoder = LabelEncoder()
label_encoder = label_encoder.fit(ylabel_train)
ylabel_train = label_encoder.transform(ylabel_train)
ylabel_test = label_encoder.transform(ylabel_test)
ytarget_train = X_train['target']
ytarget_test = X_test['target']
X_train.drop(columns=['target'],inplace=True)
X_test.drop(columns=['target'],inplace=True)
# Creando dataset completo para entrenar los mejores pipelines
X_full = X.drop(columns='target')
y_full_label = label_encoder.transform(y)
y_full_target = X.target
# Función para obtener la temporada del año según el release date
def season_of_date(date):
year = str(date.year)
seasons = {'spring': pd.date_range(start=year+'/03/21', end=year+'/06/20'),
'summer': pd.date_range(start=year+'/06/21', end=year+'/09/22'),
'autumn': pd.date_range(start=year+'/09/23', end=year+'/12/20')}
if date in seasons['spring']:
return 'spring'
if date in seasons['summer']:
return 'summer'
if date in seasons['autumn']:
return 'autumn'
else:
return 'winter'
## Código Feature Engineering (Opcional)
def custom_features(df,top_artistas=top_artistas,top_productoras=top_productoras,top_keywords=top_keywords):
df_copy = df.copy()
# Codeando fecha
df_copy['release_month'] = df_copy['release_date'].dt.month
df_copy['season'] = df_copy.release_date.map(season_of_date)
# Contando personajes celebres por pelicula
df_copy['n_celebrities'] = df_copy['credits'].apply(lambda x: len(set(x.split('-')) & set(top_artistas[:,0])))
df_copy['celebrities'] = df_copy['credits'].apply(lambda x: list((set(x.split('-')) & set(top_artistas[:,0])))
if len(set(x.split('-')) & set(top_artistas[:,0]))>=1 else [])
# Contando personajes celebres por pelicula
df_copy['in_top_production_companies'] = df_copy['production_companies'].apply(lambda x: 1 if (len(set(x.split('-')) & set(top_productoras[:,0])))>=1 else 0)
# Categorizacion de keywords
df_copy['top_keyword'] = df_copy['keywords'].apply(lambda x: x.split('-')[0]
if (x.split('-')[0] in top_keywords[:,0]) else 'other')
# Ratios
df_copy['runtime_budget_ratio'] = 0
indexes = df_copy['budget']!=0
df_copy.loc[indexes,'runtime_budget_ratio'] = df_copy.loc[indexes,'runtime']/df_copy.loc[indexes,'budget']
# Conteo actores, productoras y generos
df_copy['n_actors'] = df_copy['credits'].apply(lambda x: len(x.split('-')))
df_copy['n_production_companies'] = df_copy['production_companies'].apply(lambda x: len(x.split('-')))
df_copy['n_genres'] = df_copy['genres'].apply(lambda x: len(x.split('-')))
# Top genre + keyword
df_copy['genre_keyword'] = df_copy['genres'].apply(lambda x: (x.split('-'))[0]) + '-' + df_copy['top_keyword']
return df_copy
custom_data = custom_features(full_data)
custom_data.hist(figsize=(15,11))
array([[<AxesSubplot:title={'center':'budget'}>,
<AxesSubplot:title={'center':'target'}>,
<AxesSubplot:title={'center':'runtime'}>],
[<AxesSubplot:title={'center':'release_date'}>,
<AxesSubplot:title={'center':'release_month'}>,
<AxesSubplot:title={'center':'n_celebrities'}>],
[<AxesSubplot:title={'center':'in_top_production_companies'}>,
<AxesSubplot:title={'center':'runtime_budget_ratio'}>,
<AxesSubplot:title={'center':'n_actors'}>],
[<AxesSubplot:title={'center':'n_production_companies'}>,
<AxesSubplot:title={'center':'n_genres'}>, <AxesSubplot:>]],
dtype=object)
custom_data.dtypes
title object budget float64 target float64 runtime float64 status object tagline object credits object genres object original_language object overview object production_companies object release_date datetime64[ns] keywords object label category release_month int64 season object n_celebrities int64 celebrities object in_top_production_companies int64 top_keyword object runtime_budget_ratio float64 n_actors int64 n_production_companies int64 n_genres int64 genre_keyword object dtype: object
# Generamos tokenizador
stop_words = stopwords.words('english')
# Definimos un tokenizador con Stemming
class StemmerTokenizer:
def __init__(self):
self.ps = PorterStemmer()
def __call__(self, doc):
doc_tok = word_tokenize(doc)
doc_tok = [t for t in doc_tok if t not in stop_words]
return [self.ps.stem(t) for t in doc_tok]
## Código ColumnTransformer
coltransf = ColumnTransformer([
("MinMaxScaler",MinMaxScaler(),['budget','n_celebrities','runtime_budget_ratio','n_actors',
'n_production_companies']),
("StandardScaler",StandardScaler(),['runtime','n_genres']),
("OneHot",OneHotEncoder(handle_unknown='ignore'),['original_language','season','top_keyword',
'in_top_production_companies','genre_keyword']),
("Ordinal",OrdinalEncoder(),['release_month']),
('BoW_uni_tri_overview',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,3)),'overview'),
('BoW_uni_tri_tagline',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,3)),'tagline')
])
Comentarios sobre preparación de datos
Se construye un holdout para el dataset original, tomando en consideración el no utilizar las columnas target y labels para el entrenamiento, y almacenándolas como ground-truth labels para cada task que se trabajará posteriormente.
Se decidió por la implementación de un Custom Feature Engineering, donde éste tiene la presencia de features adicionales con relación a: mes de publicación, estación del año de publicación, cantidad de artistas del top 50, artistas del top 50, presencia de productora top 50, primera keyword presente (o más representativa), couciente entre duración de película y budget, cantidad de actores, cantidad de productoras, cantidad de géneros, categoría de género-keyword con las primeras presentes de cada una (ejemplo: Action-Vampire). De esta forma al aplicar sobre el dataset previamente limpio, se obtienen un total de 25 features previo a la implementación de un column transformer.
Para la implementación de un column transformer se tomaron en consideración las siguientes aplicaciones: MinMaxScaler, StandardScaler, One-Hot Encoder, Ordinal Encoder, y CountVectorizer mediante Bag of Words en n_gramas para las features de overview y tagline. Cada una de estas aplicaciones fue realizada según criterio de grupo y para toda feature que no fue transformada, ésta quedará fuera de la base de entrenamiento.
target_names = full_data['label'].unique()
def evaluation_cls(pipeline,X_train,y_train,X_test,y_test,target_names,classifier_name):
print(f'Pipeline: {classifier_name}')
pipeline.fit(X_train, y_train)
y_pred = pipeline.predict(X_test)
print(classification_report(y_test, y_pred, target_names=target_names,zero_division=0),'\n')
f1 = round(f1_score(y_test, y_pred,average='macro'), 3)
print("F1 Score:", f1, end='\t')
kappa = round(cohen_kappa_score(y_test, y_pred), 3)
print("Kappa:", kappa, end='\t')
accuracy = round(accuracy_score(y_test, y_pred), 3)
print("Accuracy:", accuracy, '\n \n')
## Código Dummy
pipe_dummy_cls = Pipeline([
('CustomFeatures',FunctionTransformer(custom_features)),
("Preprocess",coltransf),
("Selection", SelectPercentile(f_classif, percentile=40)),
('clf',DummyClassifier(strategy="stratified",random_state=7)),
])
## Código Clasificador
pipe_dTree_cls = Pipeline([
('CustomFeatures',FunctionTransformer(custom_features)),
("Preprocess",coltransf),
("Selection", SelectPercentile(f_classif, percentile=40)),
('clf',DecisionTreeClassifier(criterion="entropy",random_state=7)),
])
## Código Comparación de métricas
evaluation_cls(pipe_dummy_cls,X_train,ylabel_train,X_test,ylabel_test,target_names,'DummyClassifier')
evaluation_cls(pipe_dTree_cls,X_train,ylabel_train,X_test,ylabel_test,target_names,'DecisionTreeClassifier')
Pipeline: DummyClassifier
precision recall f1-score support
Mostly Positive 0.23 0.21 0.22 268
Positive 0.49 0.50 0.50 597
Very Positive 0.00 0.00 0.00 35
Mixed 0.29 0.29 0.29 354
Negative 0.02 0.03 0.02 37
accuracy 0.36 1291
macro avg 0.21 0.21 0.21 1291
weighted avg 0.35 0.36 0.36 1291
F1 Score: 0.206 Kappa: 0.031 Accuracy: 0.356
Pipeline: DecisionTreeClassifier
precision recall f1-score support
Mostly Positive 0.29 0.26 0.28 268
Positive 0.49 0.53 0.51 597
Very Positive 0.12 0.09 0.10 35
Mixed 0.43 0.45 0.44 354
Negative 0.25 0.08 0.12 37
accuracy 0.43 1291
macro avg 0.32 0.28 0.29 1291
weighted avg 0.42 0.43 0.42 1291
F1 Score: 0.29 Kappa: 0.12 Accuracy: 0.427
Se puede observar como para un modelo básico como Decision Tree inicializado de forma automática, sin tunning de hiperparámetros, se llega a obtener un mejor resultado que un modelo Dummy, pero de todas formas será necesaria la implementación de gridsearch y elección de múltiples modelos, tanto simples como más complejos, para mejorar este desempeño.
### Código GridSearch Modelos Tipicos
coltransf_grid_base_cls = ColumnTransformer([
("MinMaxScaler",MinMaxScaler(),['budget','n_celebrities','runtime_budget_ratio','n_actors',
'n_production_companies']),
("StandardScaler",StandardScaler(),['runtime','n_genres']),
("OneHot",OneHotEncoder(handle_unknown='ignore'),['original_language','season','top_keyword',
'in_top_production_companies','genre_keyword']),
("Ordinal",OrdinalEncoder(),['release_month']),
('BoW_overview',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,3)),'overview'),
('BoW_tagline',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,3)),'tagline'),
])
pipe_grid_base_cls = Pipeline(
[ ('CustomFeatures',FunctionTransformer(custom_features)),
("preprocessing", coltransf_grid_base_cls),
("selection", SelectPercentile(f_classif)),
("model", SVC()),
]
)
grid_base_cls = [
# grilla 1: SVC
{ "preprocessing__BoW_overview__ngram_range": [(1,2),(1,3),(2,3)],
"selection__percentile": [40,60,80],
"model": [SVC(random_state=7)],
"model__kernel": ["rbf", "linear"],
"model__C": [1,10]
},
# grilla 2: Random Forest
{ "preprocessing__BoW_overview__ngram_range": [(1,2),(1,3),(2,3)],
"selection__percentile": [40,60,80],
"model": [RandomForestClassifier(random_state=7)],
"model__n_estimators": [30,100],
"model__max_depth": [15,None]
},
# grilla 3: Decision Tree
{ "preprocessing__BoW_overview__ngram_range": [(1,1),(1,2),(2,3)],
"selection__percentile": [40,60,80],
"model": [DecisionTreeClassifier(random_state=7)],
"model__criterion": ['gini','entropy','log_loss'],
"model__max_depth": [15,None]
},
]
gs = GridSearchCV(pipe_grid_base_cls, grid_base_cls, n_jobs=-1, scoring="f1_macro",verbose=10).fit(X_train, ylabel_train)
gs.best_score_
0.29898709726015393
gs.best_estimator_
Pipeline(steps=[('CustomFeatures',
FunctionTransformer(func=<function custom_features at 0x0000022472FA8DC0>)),
('preprocessing',
ColumnTransformer(transformers=[('MinMaxScaler',
MinMaxScaler(),
['budget', 'n_celebrities',
'runtime_budget_ratio',
'n_actors',
'n_production_companies']),
('StandardScaler',
StandardScaler(),
['runtime', 'n_genres']),
('OneHot',
OneHotE...
('BoW_overview',
CountVectorizer(tokenizer=<__main__.StemmerTokenizer object at 0x0000022403E55D90>),
'overview'),
('BoW_tagline',
CountVectorizer(ngram_range=(1,
3),
tokenizer=<__main__.StemmerTokenizer object at 0x0000022403E55F40>),
'tagline')])),
('selection', SelectPercentile(percentile=60)),
('model',
DecisionTreeClassifier(criterion='entropy', random_state=7))])
coltransf_best_base_cls = ColumnTransformer([
("MinMaxScaler",MinMaxScaler(),['budget','n_celebrities','runtime_budget_ratio','n_actors',
'n_production_companies']),
("StandardScaler",StandardScaler(),['runtime','n_genres']),
("OneHot",OneHotEncoder(handle_unknown='ignore'),['original_language','season','top_keyword',
'in_top_production_companies','genre_keyword']),
("Ordinal",OrdinalEncoder(),['release_month']),
('BoW_overview',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,1)),'overview'),
('BoW_tagline',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,3)),'tagline'),
])
pipe_best_base_cls = Pipeline(
[ ('CustomFeatures',FunctionTransformer(custom_features)),
("preprocessing", coltransf_best_base_cls),
("selection", SelectPercentile(f_classif,percentile=60)),
("model", DecisionTreeClassifier(criterion='entropy', random_state=7)),
]
)
evaluation_cls(pipe_best_base_cls,X_train,ylabel_train,X_test,ylabel_test,target_names,'DecisionTreeClassifier')
Pipeline: DecisionTreeClassifier
precision recall f1-score support
Mostly Positive 0.31 0.30 0.30 268
Positive 0.49 0.49 0.49 597
Very Positive 0.08 0.06 0.07 35
Mixed 0.38 0.41 0.39 354
Negative 0.12 0.08 0.10 37
accuracy 0.40 1291
macro avg 0.27 0.27 0.27 1291
weighted avg 0.40 0.40 0.40 1291
F1 Score: 0.269 Kappa: 0.098 Accuracy: 0.403
pipe_xgboost_cls = Pipeline([
('CustomFeatures',FunctionTransformer(custom_features)),
("Preprocess",coltransf),
("Selection", SelectPercentile(f_classif, percentile=90)),
('clf',XGBClassifier(random_state=7)),
])
pipe_lgbm_cls = Pipeline([
('CustomFeatures',FunctionTransformer(custom_features)),
("Preprocess",coltransf),
("Selection", SelectPercentile(f_classif, percentile=90)),
('clf',LGBMClassifier(random_state=7)),
])
Pipeline: LightGBMClassifier
precision recall f1-score support
Mostly Positive 0.41 0.26 0.32 268
Positive 0.51 0.67 0.58 597
Very Positive 0.00 0.00 0.00 35
Mixed 0.52 0.48 0.50 354
Negative 0.00 0.00 0.00 37
accuracy 0.50 1291
macro avg 0.29 0.28 0.28 1291
weighted avg 0.46 0.50 0.47 1291
F1 Score: 0.28 Kappa: 0.191 Accuracy: 0.497
## Código Comparación de métricas
start_time = time.time()
evaluation_cls(pipe_xgboost_cls,X_train,ylabel_train,X_test,ylabel_test,target_names,'XGBoostClassifier')
evaluation_cls(pipe_lgbm_cls,X_train,ylabel_train,X_test,ylabel_test,target_names,'LightGBMClassifier')
print(f'Tiempo de duración: {time.time()-start_time}')
Pipeline: XGBoostClassifier
precision recall f1-score support
Mostly Positive 0.39 0.22 0.29 268
Positive 0.51 0.71 0.59 597
Very Positive 0.00 0.00 0.00 35
Mixed 0.53 0.45 0.49 354
Negative 0.67 0.05 0.10 37
accuracy 0.50 1291
macro avg 0.42 0.29 0.29 1291
weighted avg 0.48 0.50 0.47 1291
F1 Score: 0.292 Kappa: 0.181 Accuracy: 0.497
Pipeline: LightGBMClassifier
[LightGBM] [Warning] Auto-choosing col-wise multi-threading, the overhead of testing was 0.017483 seconds.
You can set `force_col_wise=true` to remove the overhead.
[LightGBM] [Info] Total Bins 5337
[LightGBM] [Info] Number of data points in the train set: 5160, number of used features: 1647
[LightGBM] [Info] Start training from score -1.571411
[LightGBM] [Info] Start training from score -0.771318
[LightGBM] [Info] Start training from score -3.607049
[LightGBM] [Info] Start training from score -1.294514
[LightGBM] [Info] Start training from score -3.551480
precision recall f1-score support
Mostly Positive 0.41 0.26 0.32 268
Positive 0.51 0.67 0.58 597
Very Positive 0.00 0.00 0.00 35
Mixed 0.52 0.48 0.50 354
Negative 0.00 0.00 0.00 37
accuracy 0.50 1291
macro avg 0.29 0.28 0.28 1291
weighted avg 0.46 0.50 0.47 1291
F1 Score: 0.28 Kappa: 0.191 Accuracy: 0.497
Tiempo de duración: 38.90761113166809
### Código GridSearch Modelos XGBoost y LightGBM
coltransf_grid_boost = ColumnTransformer([
("MinMaxScaler",MinMaxScaler(),['budget','n_celebrities','runtime_budget_ratio','n_actors',
'n_production_companies']),
("StandardScaler",StandardScaler(),['runtime','n_genres']),
("OneHot",OneHotEncoder(handle_unknown='ignore'),['original_language','season','top_keyword',
'in_top_production_companies','genre_keyword']),
("Ordinal",OrdinalEncoder(),['release_month'])
])
pipe_grid_boost_cls = Pipeline(
[ ('CustomFeatures',FunctionTransformer(custom_features)),
("preprocessing", coltransf_grid_boost),
("selection", SelectPercentile(f_classif, percentile=90)),
("model", LGBMClassifier(random_state=7)),
]
)
grid_boosting_cls = [
# grilla 1: XGBoost
{ "model": [XGBClassifier(random_state=7)],
"model__subsample": [0.5, 1],
"model__colsample_bytree": [0.5, 1],
"model__learning_rate": [0.3, 0.03],
"model__max_depth": [2, 12],
"model__n_estimators": [100],
"model__min_child_weight": [1,5,15],
},
# grilla 2: LightGBM
{ "model": [LGBMClassifier(random_state=7)],
"model__boosting_type": ['gbdt','dart'],
"model__n_estimators": [8,24],
"model__learning_rate": [0.005, 0.01],
"model__colsample_bytree": [0.64, 0.66],
"model__subsample": [0.7,0.75]
},
]
gs_boost_cls = GridSearchCV(pipe_grid_boost_cls, grid_boosting_cls, n_jobs=-1, scoring="f1_macro",verbose=10).fit(X_train, ylabel_train)
Fitting 5 folds for each of 80 candidates, totalling 400 fits
gs_boost_cls.best_score_
0.3147265518100161
gs_boost_cls.best_params_
{'model': XGBClassifier(base_score=None, booster=None, callbacks=None,
colsample_bylevel=None, colsample_bynode=None, colsample_bytree=1,
early_stopping_rounds=None, enable_categorical=False,
eval_metric=None, gamma=None, gpu_id=None, grow_policy=None,
importance_type=None, interaction_constraints=None,
learning_rate=0.3, max_bin=None, max_cat_to_onehot=None,
max_delta_step=None, max_depth=12, max_leaves=None,
min_child_weight=5, missing=nan, monotone_constraints=None,
n_estimators=100, n_jobs=None, num_parallel_tree=None,
predictor=None, random_state=7, reg_alpha=None, reg_lambda=None, ...),
'model__colsample_bytree': 1,
'model__learning_rate': 0.3,
'model__max_depth': 12,
'model__min_child_weight': 5,
'model__n_estimators': 100,
'model__subsample': 1}
coltransf_best = ColumnTransformer([
("MinMaxScaler",MinMaxScaler(),['budget','n_celebrities','runtime_budget_ratio','n_actors',
'n_production_companies']),
("StandardScaler",StandardScaler(),['runtime','n_genres']),
("OneHot",OneHotEncoder(handle_unknown='ignore'),['original_language','top_keyword',
'in_top_production_companies','genre_keyword']),
# ("Ordinal",OrdinalEncoder(),['release_month']),
# ('BoW_overview',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,3)),'overview'),
# ('BoW_tagline',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,3)),'tagline'),
])
# release_month,'season',
best_pipe_cls = Pipeline(
[ ('CustomFeatures',FunctionTransformer(custom_features)),
("preprocessing", coltransf_best),
("selection", SelectPercentile(f_classif,percentile=65)),
("model", XGBClassifier(subsample=1, random_state=7,colsample_bytree=1,learning_rate=0.3,max_depth=12,
min_child_weight=5, n_estimators=100)),
]
)
evaluation_cls(best_pipe_cls,X_train,ylabel_train,X_test,ylabel_test,target_names,'XGBClassifier')
Pipeline: XGBClassifier
precision recall f1-score support
Mostly Positive 0.38 0.30 0.33 268
Positive 0.53 0.66 0.59 597
Very Positive 0.43 0.09 0.14 35
Mixed 0.52 0.47 0.50 354
Negative 0.25 0.05 0.09 37
accuracy 0.50 1291
macro avg 0.42 0.31 0.33 1291
weighted avg 0.48 0.50 0.48 1291
F1 Score: 0.33 Kappa: 0.207 Accuracy: 0.5
coltransf_best = ColumnTransformer([
("MinMaxScaler",MinMaxScaler(),['budget','n_celebrities','runtime_budget_ratio','n_actors',
'n_production_companies']),
("StandardScaler",StandardScaler(),['runtime','n_genres']),
("OneHot",OneHotEncoder(handle_unknown='ignore'),['original_language','top_keyword',
'in_top_production_companies','genre_keyword'])
])
# release_month,'season'
best_pipe_cls = Pipeline(
[ ('CustomFeatures',FunctionTransformer(custom_features)),
("preprocessing", coltransf_best),
("selection", SelectPercentile(f_classif,percentile=65)),
("model", XGBClassifier(subsample=1, random_state=7,colsample_bytree=1,learning_rate=0.3,max_depth=12,
min_child_weight=5, n_estimators=100)),
]
)
best_pipe_cls.fit(X_full, y_full_label)
Pipeline(steps=[('CustomFeatures',
FunctionTransformer(func=<function custom_features at 0x0000022472FA8DC0>)),
('preprocessing',
ColumnTransformer(transformers=[('MinMaxScaler',
MinMaxScaler(),
['budget', 'n_celebrities',
'runtime_budget_ratio',
'n_actors',
'n_production_companies']),
('StandardScaler',
StandardScaler(),
['runtime', 'n_genres']),
('OneHot',
OneHotE...
gamma=0, gpu_id=-1, grow_policy='depthwise',
importance_type=None, interaction_constraints='',
learning_rate=0.3, max_bin=256,
max_cat_to_onehot=4, max_delta_step=0,
max_depth=12, max_leaves=0, min_child_weight=5,
missing=nan, monotone_constraints='()',
n_estimators=100, n_jobs=0, num_parallel_tree=1,
objective='multi:softprob', predictor='auto',
random_state=7, reg_alpha=0, ...))])
Justificación de elección de clasificador
Se realizó un gridsearch para modelos tradicionales y simples, tales como DecisionTree, SVC y Random Forest. Con esto se obtuvo un modelo con un F1-score sobre el conjunto de entrenamiento de 0.298, esto con un clasificador compuesto de DecisionTree Classifier, todas las funciones definidas del column transformer, una selección de mejores 60% de features y la aplicación de custom features, sin embargo, el desempeño sobre el conjunto de test de prueba fue de 0.269.
Posteriormente se procedió a realizar un gridsearch pero esta vez con algoritmos de tipo boosting, en este caso XGBoosting y LightGBM. Para esta busqueda se mantuvieron los atributos y funciones utilizadas para la busqueda de un modelo más simple. De esta forma el algoritmo XGBoosting con sus parámetros encontrados obtuvo un F1-score de 0.314 sobre el conjunto de entrenamiento, es por esto que se decidió proceder a un fine-tunning de custom features con esta implementación.
Con el mejor modelo encontrado en un gridsearch de algoritmos de boosting, se procede a eliminar y/o modificar ciertas features que podrían perjudicar el desempeño. Tras múltiples experimentos y pruebas se llegó al mejor resultado mediante la eliminación de features relacionadas a fechas, en este caso 'release_month' y 'season', además se optó por remover de forma completa toda utilización de count-vectorizer o procesamiento sobre columnas de texto mediante Bag of Words. Finalmente se decide utilizar un select-percentil de features sobre el 65% de estas, lo que en conjunto entrega un F1-score de 0.33 sobre el conjunto de test.
Para efectos de predicción para la competencia, se decide realizar una base de datos más grande, mezclando los conjuntos previos de entrenamiento y test, sin considerar las columnas de 'label' o 'target' debido a su posible overfitting, de esta forma se aumenta la cantidad de instancias para entrenar. Se corroboró con la entrega de estas predicciones que se obtiene un mejor resultado con el conjunto de competencia que es independiente a los de prueba local.
def evaluation_reg(pipeline,X_train,y_train,X_test,y_test,regressor_name):
print(f'Pipeline: {regressor_name}')
pipeline.fit(X_train, y_train)
y_pred_reg = pipeline.predict(X_test)
R2 = r2_score(y_test, y_pred_reg)
print(f'Valor de R2: {R2}','\n')
## Código Dummy
pipe_dummy_reg = Pipeline([
('CustomFeatures',FunctionTransformer(custom_features)),
("Preprocess",coltransf),
("Selection", SelectPercentile(f_regression, percentile=90)),
('reg',DummyRegressor(strategy="mean")),
])
## Código Regresor
pipe_dTree_reg = Pipeline([
('CustomFeatures',FunctionTransformer(custom_features)),
("Preprocess",coltransf),
("Selection", SelectPercentile(f_regression, percentile=90)),
('reg',DecisionTreeRegressor(criterion="squared_error",random_state=7)),
])
## Código Comparación de métricas
evaluation_reg(pipe_dummy_reg,X_train,ytarget_train,X_test,ytarget_test,'DummyRegressor')
evaluation_reg(pipe_dTree_reg,X_train,ytarget_train,X_test,ytarget_test,'DecisionTreeRegressor')
Pipeline: DummyRegressor Valor de R2: -0.0010585663913631471 Pipeline: DecisionTreeRegressor Valor de R2: 0.3052435078623271
Claramente la utilización de regresión con un modelo simple como lo es Decision Tree, de forma automática, entrega mejor resultados de partida respecto a la métrica R2 en comparación con un regresor Dummy, de todas formas este nivel de desempeño es bajo y es necesario encontrar un conjunto de parámetros y modelos para optimizar.
### Código GridSearch
coltransf_grid_base_reg = ColumnTransformer([
("MinMaxScaler",MinMaxScaler(),['budget','n_celebrities','runtime_budget_ratio','n_actors',
'n_production_companies']),
("StandardScaler",StandardScaler(),['runtime','n_genres']),
("OneHot",OneHotEncoder(handle_unknown='ignore'),['original_language','season','top_keyword',
'in_top_production_companies','genre_keyword']),
("Ordinal",OrdinalEncoder(),['release_month']),
('BoW_overview',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,1)),'overview'),
('BoW_tagline',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,3)),'tagline'),
])
pipe_grid_base_reg = Pipeline(
[ ('CustomFeatures',FunctionTransformer(custom_features)),
("preprocessing", coltransf_grid_1),
("selection", SelectPercentile(f_regression)),
("model", SVR()),
]
)
grid_base_reg = [
# grilla 1: SVR
{ "preprocessing__BoW_overview__ngram_range": [(1,2),(1,3)],
"selection__percentile": [40,60,80],
"model": [SVR()],
"model__kernel": ["rbf", "linear"],
"model__C": [1,10]
},
# grilla 2: ElasticNet
{ "preprocessing__BoW_overview__ngram_range": [(1,2),(1,3)],
"selection__percentile": [40,60,80],
"model": [ElasticNet(random_state=7)],
"model__alpha": [1e-2, 1e-1, 1],
"model__fit_intercept": [True, False],
"model__max_iter": [100, 1000]
},
# grilla 3: DecisionTree Regressor
{ "preprocessing__BoW_overview__ngram_range": [(1,2),(1,3)],
"selection__percentile": [40,60,80],
"model": [DecisionTreeRegressor(random_state=7)],
"model__criterion": ['squared_error', 'friedman_mse', 'absolute_error'],
"model__max_depth": [15,None]
},
]
gs_base_reg = GridSearchCV(pipe_grid_base_reg, grid_base_reg, n_jobs=-1, scoring='r2',verbose=10).fit(X_train, ytarget_train)
pipe_xgboost_reg = Pipeline([
('CustomFeatures',FunctionTransformer(custom_features)),
("Preprocess",coltransf),
("Selection", SelectPercentile(f_regression, percentile=90)),
('reg',XGBRegressor(random_state=7)),
])
pipe_lgbm_reg = Pipeline([
('CustomFeatures',FunctionTransformer(custom_features)),
("Preprocess",coltransf),
("Selection", SelectPercentile(f_regression, percentile=90)),
('reg',LGBMRegressor(random_state=7)),
])
## Código Comparación de métricas
start_time = time.time()
evaluation_reg(pipe_xgboost_reg,X_train,ytarget_train,X_test,ytarget_test,'XGBoostRegressor')
evaluation_reg(pipe_lgbm_reg,X_train,ytarget_train,X_test,ytarget_test,'LightGBMRegressor')
print(f'Tiempo de duración: {time.time()-start_time}')
Pipeline: XGBoostRegressor Valor de R2: 0.528560233966219 Pipeline: LightGBMRegressor [LightGBM] [Warning] Auto-choosing row-wise multi-threading, the overhead of testing was 0.008478 seconds. You can set `force_row_wise=true` to remove the overhead. And if memory is not enough, you can set `force_col_wise=true`. [LightGBM] [Info] Total Bins 5316 [LightGBM] [Info] Number of data points in the train set: 5160, number of used features: 1640 [LightGBM] [Info] Start training from score 91288722.518411 Valor de R2: 0.5421598692147556 Tiempo de duración: 25.13885235786438
coltransf_grid_boost_reg = ColumnTransformer([
("MinMaxScaler",MinMaxScaler(),['budget','n_celebrities','runtime_budget_ratio','n_actors',
'n_production_companies']),
("StandardScaler",StandardScaler(),['runtime','n_genres']),
("OneHot",OneHotEncoder(handle_unknown='ignore'),['original_language','season','top_keyword',
'in_top_production_companies','genre_keyword']),
("Ordinal",OrdinalEncoder(),['release_month']),
# ('BoW_overview',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,2)),'overview'),
# ('BoW_tagline',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,3)),'tagline'),
])
pipe_grid_boost_reg = Pipeline(
[ ('CustomFeatures',FunctionTransformer(custom_features)),
("preprocessing", coltransf_grid_boost_reg),
("selection", SelectPercentile(f_regression,percentile=90)),
("model", LGBMRegressor(random_state=7)),
]
)
grid_boosting_reg = [
# grilla 1: XGBoost
{ "model": [XGBRegressor(random_state=7)],
"model__learning_rate": [0.05, 0.10],
"model__max_depth": [3, 8],
"model__min_child_weight": [1, 7],
"model__gamma": [0.0, 0.2],
"model__colsample_bytree": [0.3, 0.4]
},
# grilla 2: LightGBM
{ "model": [LGBMRegressor(random_state=7)],
"model__num_leaves": [7, 50],
"model__n_estimators": [50,200],
"model__learning_rate": [0.1, 0.003],
"model__max_depth": [-1, 5]
},
]
gs_boost_reg = GridSearchCV(pipe_grid_boost_reg, grid_boosting_reg, n_jobs=-1, scoring="r2",verbose=10).fit(X_train, ytarget_train)
Fitting 5 folds for each of 48 candidates, totalling 240 fits
gs_boost_reg.best_score_
0.5903709251984217
gs_boost_reg.best_estimator_[3]
XGBRegressor(base_score=0.5, booster='gbtree', callbacks=None,
colsample_bylevel=1, colsample_bynode=1, colsample_bytree=0.4,
early_stopping_rounds=None, enable_categorical=False,
eval_metric=None, gamma=0.0, gpu_id=-1, grow_policy='depthwise',
importance_type=None, interaction_constraints='',
learning_rate=0.05, max_bin=256, max_cat_to_onehot=4,
max_delta_step=0, max_depth=8, max_leaves=0, min_child_weight=1,
missing=nan, monotone_constraints='()', n_estimators=100, n_jobs=0,
num_parallel_tree=1, predictor='auto', random_state=7, reg_alpha=0,
reg_lambda=1, ...)
coltransf_best_boost_reg = ColumnTransformer([
("MinMaxScaler",MinMaxScaler(),['budget','n_celebrities','runtime_budget_ratio','n_actors',
'n_production_companies']),
("StandardScaler",StandardScaler(),['runtime','n_genres']),
("OneHot",OneHotEncoder(handle_unknown='ignore'),['original_language','season','top_keyword',
'in_top_production_companies','genre_keyword']),
("Ordinal",OrdinalEncoder(),['release_month'])
])
pipe_best_boost_reg = Pipeline(
[ ('CustomFeatures',FunctionTransformer(custom_features)),
("preprocessing", coltransf_best_boost_reg),
("selection", SelectPercentile(f_regression,percentile=90)),
("model", XGBRegressor(random_state=7,learning_rate=0.05,max_depth=8,min_child_weight=1,
gamma=0.0,colsample_bytree=0.4)),
]
)
evaluation_reg(pipe_best_boost_reg,X_train,ytarget_train,X_test,ytarget_test,'XGBRegressor')
Pipeline: XGBRegressor Valor de R2: 0.5661824539456413
coltransf_best_reg = ColumnTransformer([
("MinMaxScaler",MinMaxScaler(),['budget','n_celebrities','runtime_budget_ratio','n_actors',
'n_production_companies']),
("StandardScaler",StandardScaler(),['runtime','n_genres']),
("OneHot",OneHotEncoder(handle_unknown='ignore'),['original_language','season','top_keyword',
'in_top_production_companies','genre_keyword']),
("Ordinal",OrdinalEncoder(),['release_month']),
('BoW_overview',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,3)),'overview'),
('BoW_tagline',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,3)),'tagline')
])
best_pipe_reg = Pipeline(
[ ('CustomFeatures',FunctionTransformer(custom_features)),
("preprocessing", coltransf_best_reg),
("selection", SelectPercentile(f_regression,percentile=90)),
("model", XGBRegressor(random_state=7,learning_rate=0.05,max_depth=8,min_child_weight=1,
gamma=0.0,colsample_bytree=0.4)),
]
)
evaluation_reg(best_pipe_reg,X_train,ytarget_train,X_test,ytarget_test,'XGBRegressor')
Pipeline: XGBRegressor Valor de R2: 0.5677415589552661
coltransf_best_reg = ColumnTransformer([
("MinMaxScaler",MinMaxScaler(),['budget','n_celebrities','runtime_budget_ratio','n_actors',
'n_production_companies']),
("StandardScaler",StandardScaler(),['runtime','n_genres']),
("OneHot",OneHotEncoder(handle_unknown='ignore'),['original_language','season','top_keyword',
'in_top_production_companies','genre_keyword']),
("Ordinal",OrdinalEncoder(),['release_month']),
('BoW_overview',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,3)),'overview'),
('BoW_tagline',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,3)),'tagline')
])
best_pipe_reg = Pipeline(
[ ('CustomFeatures',FunctionTransformer(custom_features)),
("preprocessing", coltransf_best_reg),
("selection", SelectPercentile(f_regression,percentile=90)),
("model", XGBRegressor(random_state=7,learning_rate=0.05,max_depth=8,min_child_weight=1,
gamma=0.0,colsample_bytree=0.4)),
]
)
best_pipe_reg.fit(X_full,y_full_target)
Pipeline(steps=[('CustomFeatures',
FunctionTransformer(func=<function custom_features at 0x0000022472FA8DC0>)),
('preprocessing',
ColumnTransformer(transformers=[('MinMaxScaler',
MinMaxScaler(),
['budget', 'n_celebrities',
'runtime_budget_ratio',
'n_actors',
'n_production_companies']),
('StandardScaler',
StandardScaler(),
['runtime', 'n_genres']),
('OneHot',
OneHotE...
gamma=0.0, gpu_id=-1, grow_policy='depthwise',
importance_type=None, interaction_constraints='',
learning_rate=0.05, max_bin=256,
max_cat_to_onehot=4, max_delta_step=0,
max_depth=8, max_leaves=0, min_child_weight=1,
missing=nan, monotone_constraints='()',
n_estimators=100, n_jobs=0, num_parallel_tree=1,
predictor='auto', random_state=7, reg_alpha=0,
reg_lambda=1, ...))])
Justificación para elección de mejor modelo de regresión para la competencia
Contemplando que la utilización de regresores mediante boosting con parámetros automáticos entregó mucho mejores resultados que los regresores más básicos, y con fines de optimización en el tiempo de ejecución, se decidió sólo hacer un gridsearch sobre los algoritmos más complejos, de esta forma la grilla contempla encontrar los mejores parámetros para los modelos de XGBoostRegressor y LightGBMRegressor. Cabe destacar que para este gridsearch, no se incluyeron los procesamientos mediante BoW o una grilla de percentiles, ya que se evalúan posteriormente.
Tras finalizar el procesamiento de la grilla, se encontró un conjunto de parámetros para el algoritmo de XGBRegressor que entregaban un R2 de 0.59 para el conjunto de entrenamiento, el cuál era levemente menor para el conjunto de test, alcanzando un 0.566.
Al elegir el modelo anteriormente descrito, se procede a realizar un fine-tunning con custom features para obtener un mejor desempeño, en este caso, a diferencia de los modelos de clasificación, el incluír todas las custom features creadas anteriormente, incluyendo los trigramas por BoW, mejoró levemente el desempeño del modelo, con un R2 de 0.567.
Considerando todo lo anterior, y de igual forma que para el clasificador, se procede a utilizar una base de datoas más grande para entrenar el modelo de regresión final para la predicción de la competencia, el que se confirmó que entregó mejores resultados.
En conclusión, se pudieron cumplir todos los objetivos planteados, puesto que los modelos implementados, tanto de clasificación como de regresión, presentaron buen rendimiento con el conjunto de test para la competencia, los cuales fueron de $F1_{score}=0.96$ y $R2_{score}=0.89$, pese a haber obtenido desempeños regularmente bajos con el conjunto de validación.
Con respecto al análisis exploratorio de datos, se puede confirmar que éste fue sumamente importante para poder generar los features con los cuales finalmente se entrenaron los modelos. Por ejemplo, conocer las distribuciones de las variables numéricas, permitió saber que tipo de escalamiento de los datos fue el correcto a utilizar para cada una de las variables. Por otro lado, determinar el top 50 de productoras, artistas y keywords llevó a la creación de features importantes para definir si una película será exitosa o no, como los n_celebrities (cantidad de celbridades), in_top_production_companies (si tienen productoras en el top 50) y top_keyword (keyword perteneciente al top 50). En general, las CustomFeatures generadas fueron bastante significativas en el desempeño de los modelos, ya que al realizar el fine tuning de éstos se lograron obtener los mejores resultados.
En particular, con respecto a la clasificación, se tiene que incialmente con un modelo baseline se llegaron a valores de F1 score bajos, sin embargo, superiores a un modelo Dummy. Tras la optimización mediante un GridSearch no se logró mejorar mucho más el desempeño del baseline, por lo que fue necesario recurrir a algoritmos de XGBoost y LightGBM, de los cuales el primero presentó mejor desmempeño. Tras realizar el fine tuning por medio de GridSearch y de forma manual, se pasó de obtener para el conjunto de validación 0.29 de F1 score a 0.33 con el mejor modelo XGBoost.
Por otro lado, con respecto a la regresión, se tuvieron resultados similares a la clasificación, donde el modelo baseline (DecisionTreeRegressor) superó al Dummy, llegando a alcanzar un de R2 score de 0.305. Dado que los modelos de XGBoost y LightGBM presentaron mejores resultados en la tarea de clasificación, se optó por realizar únicamente un fine tuning a estos modelos, donde el mejor desempeño lo obtuvo el XGBRegressor con un valor de R2 score igual a 0.567, el cual inlcuyó todas las custom features creadas y los trigramas en base a Bag of Words, lo cual no se tuvo para el modelo de clasificación.
Cabe destacar que para los resultados de la competencia se optó por utilizar todos los datos entregados para entrenar ambos modelos, lo cual permitió incrementar los resultados de la competencia para ambas métricas y alcanzar valores más que satisfactorios.
Finalmente, algunas posibles formas de mejorar el rendimiento de los modelos corresponde a aplicar técnicas para mejorar el desbalance de las clases, como por ejemplo Under y OverSampling. Por otro lado, con respecto a los aprendizajes generales del proyecto, consideramos que realizar un buen análisis exploratorio de datos y un fine tuning de los modelos vía GridSearch, son estrategias sumamente importantes para obtener los mejores resultados posibles.
Para subir los resultados obtenidos a la pagina de CodaLab utilice la función generateFiles entregada mas abajo. Esto es debido a que usted deberá generar archivos que respeten extrictamente el formato de CodaLab, de lo contario los resultados no se veran reflejados en la pagina de la competencia.
Para los resultados obtenidos en su modelo de clasificación y regresión, estos serán guardados en un archivo zip que contenga los archivos predicctions_clf.txt para la clasificación y predicctions_rgr.clf para la regresión. Los resultados, como se comento antes, deberan ser obtenidos en base al dataset test.pickle y en cada una de las lineas deberan presentar las predicciones realizadas.
Ejemplos de archivos:
[ ] predicctions_clf.txt
Mostly Positive
Mostly Positive
Negative
Positive
Negative
Positive
...
[ ] predicctions_rgr.txt
16103.58
16103.58
16041.89
9328.62
107976.03
194374.08
...
from zipfile import ZipFile
import os
def generateFiles(predict_data, clf_pipe, rgr_pipe):
"""Genera los archivos a subir en CodaLab
Input
predict_data: Dataframe con los datos de entrada a predecir
clf_pipe: pipeline del clf
rgr_pipe: pipeline del rgr
Ouput
archivo de txt
"""
y_pred_clf = label_encoder.inverse_transform(clf_pipe.predict(predict_data))
y_pred_rgr = rgr_pipe.predict(predict_data)
with open('./predictions_clf.txt', 'w') as f:
for item in y_pred_clf:
f.write("%s\n" % item)
with open('./predictions_rgr.txt', 'w') as f:
for item in y_pred_rgr:
f.write("%s\n" % item)
with ZipFile('predictions.zip', 'w') as zipObj2:
zipObj2.write('predictions_rgr.txt')
zipObj2.write('predictions_clf.txt')
os.remove("predictions_rgr.txt")
os.remove("predictions_clf.txt")
predict_data = pd.read_pickle('test.pickle')
predict_data.tagline = predict_data.tagline.astype(str)
predict_data.overview = predict_data.overview.astype(str)
# Ejecutar función para generar el archivo de predicciones.
# perdict_data debe tener cargada los datos del text.pickle
# mientras que clf_pipe y rgr_pipe, son los pipeline de
# clasificación y regresión respectivamente.
generateFiles(predict_data, best_pipe_cls,best_pipe_reg)
import os
os.system('jupyter nbconvert --to html Proyecto_enunciado.ipynb')
0